#include "Avalon/UidService/MySqlUidDataStore.hpp"
#include <boost/lexical_cast.hpp>
#include <mysql++/mysql++.h>
#include <mysql++/ssqls.h>
#ifdef CreateDirectory
  #undef CreateDirectory
#endif
#include "Avalon/IO/ConnectException.hpp"

using namespace Avalon;
using namespace Avalon::IO;
using namespace Avalon::Network;
using namespace Avalon::Threading;
using namespace Avalon::UidService;
using namespace boost;
using namespace boost::posix_time;
using namespace boost::signals2;
using namespace mysqlpp;
using namespace std;

namespace {
  namespace SqlInsert {
    sql_create_1(next_uid, 1, 0,
      mysqlpp::sql_int_unsigned, uid);
  }

  sql_create_1(next_uid, 1, 0,
    mysqlpp::sql_int_unsigned, uid);

  bool TableExists(mysqlpp::Connection* databaseConnection,
      const string& schema, const char* table) {
    Query query = databaseConnection->query();
    query << "SHOW TABLES IN " << schema << " LIKE " << quote << table;
    StoreQueryResult result = query.store();
    return !result.empty();
  }

  bool LoadNextUid(mysqlpp::Connection* databaseConnection,
      const string& schema) {
    if(TableExists(databaseConnection, schema, "next_uid")) {
      return true;
    }
    Query query = databaseConnection->query();
    query << "CREATE TABLE next_uid ("
      "uid INTEGER UNSIGNED NOT NULL)";
    if(!query.execute()) {
      return false;
    }
    query.reset();
    SqlInsert::next_uid nextUidRow(1);
    query.insert(nextUidRow);
    return query.execute();
  }

  bool LoadTables(mysqlpp::Connection* databaseConnection,
      const string& schema) {
    if(!LoadNextUid(databaseConnection, schema)) {
      return false;
    }
    return true;
  }
}

MySqlUidDataStore::MySqlUidDataStore(const IpAddress& address,
    const string& schema, const string& username, const string& password)
    : m_address(address),
      m_schema(schema),
      m_username(username),
      m_password(password),
      m_databaseConnection(new mysqlpp::Connection(false)),
      m_isConnected(false) {}

MySqlUidDataStore::~MySqlUidDataStore() {
  Close();
}

long long MySqlUidDataStore::GetNextUid() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  return LockedGetNextUid();
}

long long MySqlUidDataStore::Reserve(long long size) {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  long long nextUid = LockedGetNextUid();
  Query query = m_databaseConnection->query();
  query << "UPDATE next_uid SET uid = " << (nextUid + size);
  if(!query.execute()) {
    BOOST_THROW_EXCEPTION(IOException("MySQL query failed."));
  }
  return nextUid;
}

void MySqlUidDataStore::Connect() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  if(m_isConnected) {
    return;
  }

  // Connect to the database.
  bool connectionResult = m_databaseConnection->set_option(
    new mysqlpp::ReconnectOption(true));
  if(!connectionResult) {
    BOOST_THROW_EXCEPTION(IOException("Unable to set MySQL reconnect option."));
  }
  connectionResult = m_databaseConnection->connect(m_schema.c_str(),
    m_address.first.c_str(), m_username.c_str(), m_password.c_str(),
    m_address.second);
  if(!connectionResult) {
    BOOST_THROW_EXCEPTION(ConnectException(
      "Unable to connect to MySQL database."));
  }

  // Load the tables and settings.
  if(!LoadTables(m_databaseConnection.get(), m_schema)) {
    BOOST_THROW_EXCEPTION(IOException("Unable to load database tables."));
  }
  m_isConnected = true;
}

bool MySqlUidDataStore::IsConnected() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  return m_isConnected;
}

void MySqlUidDataStore::Close() {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  if(!m_isConnected) {
    return;
  }
  m_databaseConnection->disconnect();
  m_isConnected = false;
  m_closedSignal();
}

connection MySqlUidDataStore::ConnectClosedSignal(
    const ClosedSignal::slot_type& slot) {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  return m_closedSignal.connect(slot);
}

connection MySqlUidDataStore::ConnectClosedSignal(
    const ClosedSignal::slot_type& slot, bool* isConnected) {
  boost::lock_guard<boost::mutex> lock(m_mutex);
  *isConnected = m_isConnected;
  return m_closedSignal.connect(slot);
}

long long MySqlUidDataStore::LockedGetNextUid() {
  Query query = m_databaseConnection->query();
  query << "SELECT * FROM next_uid";
  StoreQueryResult result = query.store();
  if(!result || result.size() != 1) {
    BOOST_THROW_EXCEPTION(IOException("MySQL query failed."));
  }
  return result[0][0].conv<long long>(0);
}
